package chloroform

import _root_.argo.jdom._
import _root_.util.logging.Logged
import json.ConfigSessionHandlerImp
import l10n.ProjectLocale
import scaladuct._
import java.lang.String
import collection.immutable.List
import collection.mutable.ArrayStack
import javax.validation.{Path, ConstraintViolation}
import memento._
import validation.{ConstraintViolationException}
import java.util.{HashMap, Map, Locale}
import collection.Seq
import scaladuct.memento._
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}

/**
 * <p>
 * Created by IntelliJ IDEA.
 * User: zslajchrt
 * Date: Nov 14, 2010
 * Time: 9:11:31 PM
 */
object StarterConfigMessageHandler {
  def hasContextInstance(session: HttpSession): Boolean =
    session.getAttribute(classOf[StarterConfigMessageHandler].getName) != null

  def getContextInstance(session: HttpSession): (Starter[Any], Any) = {
    require(hasContextInstance(session), "No context instance available")
    session.getAttribute(classOf[StarterConfigMessageHandler].getName).asInstanceOf[(Starter[Any], Any)]
  }

  def setContextInstance(session: HttpSession, starter: Starter[Any], instance: Any) =
    session.setAttribute(classOf[StarterConfigMessageHandler].getName, (starter, instance))

  def removeContextInstance(session: HttpSession) =
    session.removeAttribute(classOf[StarterConfigMessageHandler].getName)

}

trait GatewayContext {
  val locale: Locale
  val contextName: String;
  var starter: Starter[Any]
  var instance: Any
  var validationException: ConstraintViolationException

  def destroyContext()
}

@serializable
@SerialVersionUID(1L)
class StarterConfigMessageHandler(page: ConfigPage[_], val locale: Locale, var register: ConfigPageRegister) extends MessageHandler
with Logged {

  //private[this] var hints: List[Property[Any]] = null

  type Gateway = ConfigPageGateway[Any]
    with GatewayWithHints[Any]
    with GatewayContextInt
    with MementoRecordingGateway[Any]
    //with MementoPlayingGateway[Any]
    //with ValidatingGateway[Any]
    with InjectingGateway[Any]

  @transient
  //private var pySH = PyductConfigSessionHandler
  private var pySH = ConfigSessionHandlerImp

  def pySessionHandler = if (pySH == null) {
    pySH = ConfigSessionHandlerImp
    pySH
  } else pySH


  trait GatewayContextInt extends GatewayContext {
    val locale = StarterConfigMessageHandler.this.locale
    val contextName: String;
    var starter: Starter[Any] = null
    var instance: Any = null
    var validationException: ConstraintViolationException = null

    def destroyContext() {
      if (starter != null)
        starter.destroy(instance)
    }
  }

  val defaultMementoStorage = DummyMementoStorage

  private[this] val cookieGatewayStack: ArrayStack[Gateway] = {
    val stack = new ArrayStack[Gateway]()
    val cookieGateway = newGateway(page.asInstanceOf[ConfigPage[Any]], "Main Flow", true)

    stack.push(cookieGateway)
    stack
  }

  def cookieGateway = cookieGatewayStack.top

  def getMementoStorage: MementoStorage = {
    defaultMementoStorage
  }

  private def newGateway(page: ConfigPage[Any], ctxName: String, withMementos: Boolean): Gateway = {
    val gw = new ConfigPageGateway[Any](page)
      with GatewayWithHints[Any]
      with GatewayContextInt
      with MementoRecordingGateway[Any]
      //with MementoPlayingGateway[Any]
      //with ValidatingGateway[Any]
      with InjectingGateway[Any] {
      val contextName = ctxName
      val mementoStorage = getMementoStorage

      def lookupFunction() = register

      protected def saveMemento(storedMementoConfig: StoredMementoConfig) = {
        saveMementoAsNewProjectEntry(storedMementoConfig)
      }
    }
    debug("Creating new gateway " + gw)
    gw
  }


  protected def saveMementoAsNewProjectEntry(storedMementoConfig: StoredMementoConfig) {
  }

  def isFinished(): Boolean = {
    cookieGatewayStack.isEmpty
  }


  private def createConstraintViolation(e: Exception, propertyName: String, invalidVal: AnyRef): ConstraintViolation[AnyRef] = {
    val viol = new ConstraintViolation[AnyRef] {
      def getConstraintDescriptor = null

      def getInvalidValue = invalidVal

      def getPropertyPath = new Path {
        def iterator = java.util.Collections.singleton[Path.Node](new Path.Node() {
          def getKey = propertyName

          def getIndex = 0

          def isInIterable = false

          def getName = propertyName
        }).iterator
      }

      def getLeafBean = null

      def getRootBeanClass = null

      def getRootBean = null

      def getMessageTemplate = ""

      def getMessage = e.getMessage
    }
    viol
  }

  /**
   * Transform the Map[JsonString, JsonNode] to Map[String, JsonNode].
   */
  private def transformMapWithJsonStringKey(responseMap: Map[JsonStringNode, JsonNode]): HashMap[String, AnyRef] = {
    val entriesIter = responseMap.entrySet.iterator
    val newMap = new HashMap[String, AnyRef]
    while (entriesIter.hasNext) {
      val entry = entriesIter.next
      val newKey = entry.getKey.getStringValue()
      newMap.put(newKey, entry.getValue())
    }
    newMap
  }

  private def updateCookie(responseMap: java.util.Map[JsonStringNode, JsonNode]): AnyRef = {
    if (cookieGateway.getCookie.isInstanceOf[CookieUpdateAware]) {
      val cookieUpdateAware: CookieUpdateAware = cookieGateway.getCookie.asInstanceOf[CookieUpdateAware]

      val newMap: HashMap[String, AnyRef] = transformMapWithJsonStringKey(responseMap)

      cookieUpdateAware.beforeUpdate(newMap)
      try {
        val updatedCookie = pySessionHandler.updateCookie(cookieGateway, responseMap)
        cookieUpdateAware.afterUpdate(null)
        updatedCookie
      }
      catch {
        case t: Throwable => {
          cookieUpdateAware.afterUpdate(t)
          throw t
        }
      }
    } else {
      pySessionHandler.updateCookie(cookieGateway, responseMap)
    }

  }

  def refresh(): MessageOutcome = if (cookieGateway.starter == null)
    getCookieMessage
  else
    getResult

  protected def getCookieMessage(): MessageOutcome = {
    val cookie = cookieGateway.getCookie
    if (cookie == Terminal || PropertyUtils.containsContextReferencesOnly(cookie)) {
      // short circuit
      consumeCookie(cookie)
    } else {
      JsonResponse(pySessionHandler.getCookieMessageBuilder(cookieGateway))
    }
  }

  protected def getResult(): MessageOutcome =
    if (cookieGateway.starter != null && cookieGateway.instance != null) {
      JsonResponse(pySessionHandler.getResultBuilder(cookieGateway))
    } else closeFlow

  protected def consumeCookie(updatedCookie: AnyRef): MessageOutcome = {
    cookieGateway.consumeCookie(updatedCookie) match {
      case None => getCookieMessage
      case Some(s) => {
        cookieGateway.starter = s
        cookieGateway.instance = s.newInstance
        getResult
      }
    }
  }

  def swallow(): MessageOutcome = consumeCookie(cookieGateway.getCookie)

  /**
   * This method does not call newInstance on the starter. It is assumed that the caller
   * will do it itself if needed.
   * @param cookies input JSON cookies found in the project file.
   * @param constructBuilders true if this method creates and returns a list of builders for JSON representation of cookies.
   * @returns a list of JSON builders for every cookie that was updated from the input JSON cookies.
   */
  def consumeJsonNodes(cookies: List[ProjectCookie], constructBuilders: Boolean): List[JsonNodeBuilder[JsonRootNode]] = {

    var updatedCookieBuilders: List[JsonNodeBuilder[JsonRootNode]] = Nil

    var remainingProjectCookies = cookies

    while (!remainingProjectCookies.isEmpty && cookieGateway.getCurrentPage != null) {
      val pageId = cookieGateway.id
      var projectCookie: ProjectCookie = null;

      // Find and drop non-anonymous project cookie corresponding to the current page.
      remainingProjectCookies = remainingProjectCookies.filterNot(pcookie => {
        if (pcookie.pageId == pageId) {
          projectCookie = pcookie
          true
        } else false
      })

      if (projectCookie == null && cookies.last.pageId == null) {
        // In case the corresponding non-anonymous cookie was not found try to find
        // an anonymous one. The anonymous cookie must be the last one in the list since it usually
        // represents an additional cookie (constructed from a URL without knowledge of the real page id, for example)
        projectCookie = cookies.last
        remainingProjectCookies = remainingProjectCookies.dropRight(1)
      }

      if (projectCookie == null) {
        throw new IncompleteCookiesConsumption(updatedCookieBuilders)
      }

      val cookie = cookieGateway.getCookie
      try {

        val configResult = if (cookie == Terminal || PropertyUtils.containsContextReferencesOnly(cookie)) {
          // short circuit
          //cookieGateway.consumeCookie(cookie)
          cookieGateway.consumeCookie(cookie)
        } else {

          val node = projectCookie.properties
          val updatedCookie: AnyRef = updateCookie(node)

          if (constructBuilders) {
            // Create the json builder for the updated cookie and put it into the output map.
            val cookieMessageBuilder = pySessionHandler.getCookieMessageBuilder(cookieGateway)
            updatedCookieBuilders ::= cookieMessageBuilder
          }
          cookieGateway.consumeCookie(updatedCookie)
        }

        configResult match {
          case None =>
          case Some(s) => {
            cookieGateway.starter = s
            //          cookieGateway.instance = s.newInstance
          }
        }
      }
      catch {
        case e: Exception => {
          // This special exception holds a list of all cookie json builders created before this exception was raised.
          // The list contains also the builder for the cookie whose consumption caused raising this exception.
          throw new IncompleteCookiesConsumption(updatedCookieBuilders, e)
        }
      }

    }

    if (!remainingProjectCookies.isEmpty)
      throw new IncompleteCookiesConsumption(updatedCookieBuilders)
    else
      updatedCookieBuilders.reverse
  }

  private def consumeJsonNode(responseMap: java.util.Map[JsonStringNode, JsonNode]): MessageOutcome = {
    debug("Consuming cookie for page " + cookieGateway.getCurrentPage.id + " (" + this + ")")
    val updatedCookie: AnyRef = updateCookie(responseMap)
    consumeCookie(updatedCookie)
  }

  def consume(responseMap: java.util.Map[JsonStringNode, JsonNode]): MessageOutcome = {
    try {
      cookieGateway.validationException = null
      consumeJsonNode(responseMap)
    }
    catch {
      case ve: ConstraintViolationException => {
        cookieGateway.validationException = ve
        getCookieMessage
      }
    }

  }

  def getHints(subject: String): MessageOutcome = {
    val hints = cookieGateway.hints(subject)
    JsonResponse(pySessionHandler.getHintsBuilder(cookieGateway, hints.asInstanceOf[List[Property[Nothing]]]))
  }

  def openHint(responseNode: JsonNode): MessageOutcome = {
    //val actualHints = if (hints == null) {
    val actualHints = {
      val subj = if (responseNode.isStringValue("subject"))
        responseNode.getStringValue("subject")
      else
        null
      cookieGateway.hints(subj)
    }

    // Read the new value carried by the hint response message
    val selectedHintId = responseNode.getStringValue("id")
    val valueKeyNode = JsonNodeFactories.aJsonString("value")
    val newValue = responseNode.getFields().get(valueKeyNode)

    actualHints.find(_.name == selectedHintId) match {
      case None => throw new IllegalArgumentException("no hint found for name " + selectedHintId)
      case Some(selectedHintProp) => {
        val selectedHint: Property[Any] = pySessionHandler.updateHints(cookieGateway,
          selectedHintProp.asInstanceOf[Property[Nothing]], newValue)

        val hintPage: ConfigPage[Any] = cookieGateway.hintPage(selectedHint)
        if (hintPage != null) {
          val hintGateway = newGateway(hintPage, selectedHint.name, false)
          cookieGatewayStack.push(hintGateway)

          // consume the cookies that are bundled in the message from client
          if (responseNode.isArrayNode("cookies")) {
            val iter = responseNode.getArrayNode("cookies").iterator
            while (iter.hasNext) {
              val cookieNode: JsonNode = iter.next
              consume(cookieNode.getObjectNode())
            }
          }

        }
        refresh

      }
    }

  }

  def closeFlow(): MessageOutcome = {
    cookieGateway.destroyContext
    cookieGatewayStack.pop.close
    if (cookieGatewayStack.isEmpty) {
      ResponseRenderer(new HttpRenderer {
        def render(request: HttpServletRequest, response: HttpServletResponse) = {
          StarterConfigMessageHandler.removeContextInstance(request.getSession)
        }
      })
      //pySessionHandler.getEndBuilder()
    } else refresh
  }

  def unconsume(): MessageOutcome = {
    cookieGateway.starter = null
    cookieGateway.instance = null
    cookieGateway.unconsume
    refresh
  }

  def execute(): ResponseRenderer = {
    val instance = cookieGateway.instance
    val starter = cookieGateway.starter
    ResponseRenderer(new HttpRenderer {
      def render(request: HttpServletRequest, response: HttpServletResponse) = {
        StarterConfigMessageHandler.setContextInstance(request.getSession, starter, instance)
        response.sendRedirect("../../exec")
      }
    })
  }

  def handleMessage(message: JsonRootNode): MessageOutcome = {
    val clientMsgType: String = message.getNullableStringValue("msgType")

    import StarterConfigMessageHandlerFactory.MessageType._
    values.find(_.toString == clientMsgType) match {
      case None => throw new UnsupportedOperationException("Unknown message type " + clientMsgType)

      case Some(messageType) => messageType match {
        case StartEdit => refresh
        case Refresh => refresh
        case ConsumeCookie => {
          consume(message.getObjectNode("updatedValues"))
        }
        case GetResult => {
          getResult
        }
        case GetHints => {
          val hintSubject: String = try {
            message.getStringValue("subject")
          }
          catch {
            case e: JsonNodeDoesNotMatchPathElementsException => null
          }
          getHints(hintSubject)
        }
        case OpenHint => {
          openHint(message)
        }
        case CloseFlow => {
          closeFlow
        }
        case Unconsume => {
          unconsume
        }
        case Execute => {
          execute
        }
      }
    }
  }

}

class MementoSavingStarterConfigMessageHandler(page: ConfigPage[_], locale: Locale, register: ConfigPageRegister,
                                               projectRef: ProjectRef,
                                               pageName: String,
                                               originalPreCookies: List[ProjectCookie],
                                               pageEntry: ProjectEntry)
  extends StarterConfigMessageHandler(page, locale, register) {

  private var consumedCookies: List[ProjectCookie] = Nil


  override protected def getCookieMessage() = {

    val curPage: ConfigPage[Any] = cookieGateway.getCurrentPage
    if (pageEntry.extendedEntry != null && curPage != null) {
      // For extension entries terminate the cookie flow prematurely when
      // a new page appears belonging to other entry.
      // To recognize which pages belong to the extension entry and which do not
      // compare the extension entry's id with the prefix of the current page.

      val curPageId = curPage.id

      val firstDot = curPageId.indexOf('.')
      val curPageName = curPageId.substring(0, firstDot)
      if (pageEntry.fullName != curPageName) {
        getResult
      } else {
        super.getCookieMessage
      }
    } else {
      super.getCookieMessage
    }
  }

  override def consume(responseMap: Map[JsonStringNode, JsonNode]) = {
    val pageId = cookieGateway.getCurrentPage.id
    var res = super.consume(responseMap)
    consumedCookies ::= new ProjectCookie(pageId, responseMap)

    def consumePreConfigCookies() {
      // Try to find a pre-configured cookie corresponding to the new page id. It may belong to a
      // page that could not be loaded because of a missing precedent page (often caused by new extensions attached
      // to the entry).
      val curPage: ConfigPage[Any] = cookieGateway.getCurrentPage
      if (curPage != null) {
        val newPageId = curPage.id
        originalPreCookies.find(_.pageId == newPageId) match {
          case Some(preCookie) => {
            res = super.consume(preCookie.properties)

            consumePreConfigCookies
          }
          case None =>
        }
      }
    }

    consumePreConfigCookies

    res
  }

  override def unconsume() = {
    val res = super.unconsume
    consumedCookies = consumedCookies.tail
    res
  }

  /**
   * Replace the page name part in the page id of a parameter descriptor. The replacement is done only for the page id
   * which is associated with the root page identified by the "pageName" attribute of this class.
   */
  private def replacePageNameInPageId(pageId: String, replacement: String): String = {
    // pageName '.' page-class-Id
    val firstDot = pageId.indexOf('.')
    val origPageName = pageId.substring(0, firstDot)
    // replace the id only if it is the root page name (pageName attribute)
    if (origPageName == pageName) {
      val pageClassId = pageId.substring(firstDot)
      replacement + pageClassId
    } else pageId

  }

  override protected def saveMementoAsNewProjectEntry(storedMementoConfig: StoredMementoConfig) = {
    val project: Project with ProjectLocale = projectRef.getProject
    project.extension(classOf[MutableProject]) match {
      case Some(mutableProject) => {
        var entry = project.findInPath(pageName)

        val newPersistentCookies = (originalPreCookies ::: consumedCookies.reverse).map(c => {
          // change the prefix in all pageIds that belong to the main page

          val extParentPrefix = if (entry.extendedEntry != null) {
            entry.extendedEntry.fullName + ","
          } else ""

          val newPageId = replacePageNameInPageId(c.pageId, extParentPrefix + storedMementoConfig.name)
          new ProjectCookie(newPageId, c.properties)
        })

        val newEntry = new ProjectEntry(storedMementoConfig.name, entry.factory, entry.isPublic, entry.scenarios,
          newPersistentCookies, entry.extensions)

        if (entry.extendedEntry == null) {
          mutableProject.addEntry(newEntry, true /*allow a replacement*/)
        } else {
          entry.extendedEntry.replaceExtension(newEntry)
        }

        if (!storedMementoConfig.parameterProperties.isEmpty) {
          // there are parameters in the memento
          val paramDescriptors = storedMementoConfig.parameterProperties.values.map(param => {
            val paramName = param.cookieProperty.name
            val paramClassName = Parameters.classToString(param.cookieProperty.clazz)
            val paramPageId = param.pageId
            // todo: replace the page name only in the case the pageid is of the main page
            paramName + ";" + paramClassName + ";" + replacePageNameInPageId(paramPageId, newEntry.name)
          })

          // construct param json properties
          val paramCookiesJsonProps: java.util.Map[JsonStringNode, JsonNode] = new HashMap[JsonStringNode, JsonNode]
          //referredPage
          //paramDescriptors
          val refPageKey = JsonNodeFactories.aJsonString("referredPage")
          val refPageValue = JsonNodeFactories.aJsonString(newEntry.name)

          val paramDescsKey = JsonNodeFactories.aJsonString("paramDescriptors")
          val jsonDescs: Seq[JsonNode] = paramDescriptors.map(JsonNodeFactories.aJsonString(_)).toList
          val paramDescsValue = JsonNodeFactories.aJsonArray(jsonDescs: _*)

          paramCookiesJsonProps.put(refPageKey, refPageValue)
          paramCookiesJsonProps.put(paramDescsKey, paramDescsValue)

          val paramEntryName: String = storedMementoConfig.name + "_param"
          val paramCookie = new ProjectCookie(paramEntryName + "." + classOf[Parameterizer].getName, paramCookiesJsonProps)
          val newParameterizerEntry = new ProjectEntry(paramEntryName,
            classOf[Parameterizer].getName, entry.isPublic, entry.scenarios,
            List(paramCookie), Nil)
          mutableProject.addEntry(newParameterizerEntry, true /*allow a replacement*/)
        }
      }
      case _ => error("Project is imutable")
    }
  }
}

class StarterConfigMessageHandlerFactory extends MessageHandlerFactory with Logged {

  object MessageType extends MessageTypeEnum {
    type MessageType = Value
    val StartEdit, Refresh, ConsumeCookie, GetResult, GetHints, OpenHint, CloseFlow, Unconsume, Execute = Value
  }

  type MessageHandlerType = StarterConfigMessageHandler

  //private val SESSION_ATTR: String = classOf[StarterConfigMessageHandler].getName
  private val SESSION_ATTR: String = getClass.getName

  val getHandledMessageIds = MessageType

  protected def getProjectRef(app: ConfigPageRegisterLookup): ProjectRef = {
    app.getProjectRef
  }

  protected def getConfigPageRegister(app: ConfigPageRegisterLookup, request: HttpServletRequest): ConfigPageRegister = {
    app.getConfigPageRegister(request)
  }

  def getHandler(messageType: getHandledMessageIds.Value, message: JsonRootNode, request: HttpServletRequest) = {
    var handler: StarterConfigMessageHandler = null
    val httpSession = request.getSession()
    httpSession.synchronized {

      debug("Handling JSON message: " + messageType + "(" + this + ")")

      handler = httpSession.getAttribute(SESSION_ATTR).asInstanceOf[StarterConfigMessageHandler]
      if (messageType == MessageType.StartEdit && handler != null) {
        // remove the last handler so that it can be reinitialized
        httpSession.removeAttribute(SESSION_ATTR)
        handler = null
      }

      val app: ConfigPageRegisterLookup = ConfigPageRegisterLookup.get(httpSession)
      val configPageRegister: ConfigPageRegister = getConfigPageRegister(app, request)

      if (handler == null || handler.isFinished) {
        require(messageType == MessageType.StartEdit, "Expected StartEdit, actual " + messageType)

        val fullPageName = message.getStringValue("pageName")
        val projectRef: ProjectRef = getProjectRef(app)
        val project = projectRef.getProject.extension(classOf[MutableProject]).get

        // Lock the project against changes as it is necessary that the pre-cookies used for the page initialization are
        // the same ones as those we get when calling pageEntry.cookies later.
        project.lock
        try {
          val page = configPageRegister.getConfigPage(fullPageName.split(",").reverse)

          val locale: Locale = request.getLocale
          var pageEntry = projectRef.getProject.findInPath(fullPageName)
          debug("Creating new StarterConfigMessageHandler for entry: " + pageEntry.name + ":" + pageEntry.factory + "(requested page name " + fullPageName + ")")
          handler = new MementoSavingStarterConfigMessageHandler(page, locale, configPageRegister, projectRef, fullPageName,
            pageEntry.cookies, pageEntry)
          httpSession.setAttribute(SESSION_ATTR, handler)
        }
        finally {
          project.unlock
        }
      } else {
        // update the register in the handler
        handler.register = configPageRegister
      }

    }

    handler
  }

  def returnHandler(handler: StarterConfigMessageHandler, req: HttpServletRequest) = {
    if (handler.isFinished)
      req.getSession.removeAttribute(SESSION_ATTR)
    else {
      debug("Replacing StarterConfigMessageHandler to session")
      req.getSession.setAttribute(SESSION_ATTR, handler)
    }
  }

}

object StarterConfigMessageHandlerFactory extends StarterConfigMessageHandlerFactory

object MetaStarterConfigMessageHandlerFactory extends StarterConfigMessageHandlerFactory {
  override protected def getProjectRef(app: ConfigPageRegisterLookup) = app.getMetaProjectRef

  override protected def getConfigPageRegister(app: ConfigPageRegisterLookup, request: HttpServletRequest): ConfigPageRegister = {
    app.getMetaRegister
  }
}

/**
 *  This special exception holds a list of all cookie json builders created before this exception was raised. The
 *  list contains also the builder for the cookie whose consumption caused raising this exception.
 */
class IncompleteCookiesConsumption(val cookieBuilders: List[JsonNodeBuilder[JsonRootNode]], e: Exception = null) extends Exception(e) {
}